<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>SmartLock</title>
    <link rel="manifest" href="/smartlock/manifest.json">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0" />
    <style>
        body {
            background: #c4e8ff;
            text-align: center;
        }

        button {
            font-size: 1.3em;
            margin: 6px;
            width: fit-content;
            padding: 8px;
            max-width: 100%;
            -webkit-appearance: none;
            box-shadow: 0px 0px 20px #00000033;
            border-radius: 15px;
            background-color: #59b4ff;
            color: #fff;
            border: none;
            margin: 0.4em;
            cursor: pointer;
        }

        input {
            border: none;
            background: #90caf93e;
            border-radius: 10px;
            padding: 3px;
        }

        div#message {
            background: #90caf93e;
            height: 200px;
            max-width: 500px;
            text-align: center;
            flex: auto;
            color: black;
            font-size: 1.1em;
            margin-top: 1em;
            border-radius: 15px;
            overflow-y: scroll;
        }

        div#title {
            color: #2f5597;
            background: #6cbdff93;
            text-align: center;
            margin: auto;
            width: 100%;
            position: fixed;
            top: 0;
            z-index: 1000;
            padding-top: 5px;
            padding-bottom: 5px;
            text-shadow: 3px 2px #00000022;
            box-shadow: 0px 20px 30px #006aa85d;
            display: flex;
            justify-content: center;
            /* backdrop-filter: blur(25px); */
            -webkit-backdrop-filter: blur(25px);
            transform: translateZ(0);
            height: 60px;
            font-size: 2em;
            padding-top: .5em;
        }

        div#container {
            margin-top: 6em;
        }

        .divider {
            font-size: 1.2em;
            color: darkblue;
            margin: 8px;
        }

        div#wrapper {
            display: flex;
            justify-content: center;
        }
    </style>
</head>

<body>
    <div id="title">SmartLock</div>
    <div id="container">
        <div id="wrapper"><div id="message"></div></div>
        <div class="divider">设备连接</div>
        <button onclick="deviceController.openLastLock()">一键开锁</button><br />
        <button onclick="deviceController.openLockDirect()">连接并开锁</button>
        <button onclick="deviceController.connectOnly()">仅连接设备</button><br />
        <div style="display:none;">设备电量: <span id="battery">未获取</span></div>
        <br /><br />
        
        <div class="divider">设备参数</div>
        DeviceId: <input disabled id="deviceId" /><br />
        Password: <input type="password" id="password" /><br />
        Attribute: <input id="attribute" /><br />
        OpenTime: <input id="openTime" /><br />
        CloseTime: <input id="closeTime" /><br />
        WaitTime: <input id="waitTime" /><br />
        <button onclick="deviceController.saveCurrent()">保存</button>
        <button onclick="deviceController.openLock()">开锁</button><br /><br />

        <div class="divider">修改密码</div>
        New Password: <input type="password" id="npw" /><br />
        <button onclick="deviceController.changePsw()">修改设备密码</button><br /><br />

        <div class="divider">操作</div>
        <button onclick="location.reload()">重置</button>
        <button onclick="(localStorage.savedPrefs='{}'),location.reload()">清除数据</button>
        <button onclick="caches.delete('SMLOCKv0-Main').then(()=>location.reload())">版本更新</button><br /><br />

        &copy; SmartLock v1.0 by cgluWxh.
    </div>

    <script src="crypto.js"></script>
    <script>
        const $=(e)=>document.querySelector(e);
        function log(e) {
            $("#message").innerHTML=e+"<br />"+$("#message").innerHTML;
        }
        const cmdFactory = {
            decodeRes(buffer) {
                let binary = ''
                const bytes = buffer
                const len = bytes.byteLength
                for (let i = 0; i < len; i++) {
                    binary += String.fromCharCode(bytes.getUint8(i))
                }
                return binary;
            },
            md5s(str) {
                return CryptoJS.MD5(str).toString().slice(8, 24);
            },
            aes(str) {
                let key = CryptoJS.enc.Utf8.parse("Fx4k6AWivOsLE4NI");
                let plainText = str;
                let encryptedData = CryptoJS.AES.encrypt(plainText, key, {
                    mode: CryptoJS.mode.ECB,
                    padding: CryptoJS.pad.ZeroPadding
                }).ciphertext;
                let binary_string = atob(CryptoJS.enc.Base64.stringify(encryptedData));
                let len = binary_string.length;
                let bytes = new Uint8Array(len);
                for (let i = 0; i < len; i++) {
                    bytes[i] = binary_string.charCodeAt(i);
                }
                return bytes.buffer;
            },
            genCmdOri(psw, cmd) {
                return (new Date().getTime() / 1000).toFixed(0) + this.md5s(psw) + cmd;
            },
            genCmd(psw, cmd) {
                let ori = this.genCmdOri(psw, cmd);
                return this.aes(ori);
            }
        }

        const bleAdapter = {
            working: false,
            device: null,
            server: null,
            service: null,
            charaRead: null,
            charaWrite: null,
            callback: null,
            get connected() {
                return this.device&&this.device.gatt&&this.device.gatt.connected;
            },
            getConnectedDeviceByID(id) {
                return new Promise((res,rej)=>{
                    if(!navigator.bluetooth.getDevices) rej("Not supported");
                    navigator.bluetooth.getDevices()
                        .then(arr=>{
                            for(let i=0;i<arr.length;i++) {
                                if(arr[i].id===id) {
                                    res(arr[i]);
                                    return;
                                }
                            }
                            rej("Device Not Found")
                        })
                        .catch(()=>log("Not Supported"))
                })
            },
            connect(device) {
                return new Promise((res, rej) => {
                    log("请求连接蓝牙设备...")
                    if(!device)
                        device=navigator.bluetooth.requestDevice({
                            filters: [
                                { namePrefix: "YILA" }
                            ],
                            optionalServices: [ "6e400001-b5a3-f393-e0a9-e50e24dcca9e", "6e400002-b5a3-f393-e0a9-e50e24dcca9e", "6e400003-b5a3-f393-e0a9-e50e24dcca9e" ]
                        })
                    device
                        .then((device) => {
                            log("正在连接...")
                            this.device = device;
                            const ac=new AbortController();
                            device.addEventListener('advertisementreceived', (event) => {
                                ac.abort();
                                console.log(event);
                            }, { once: true });
                            if(device.watchAdvertisements) device.watchAdvertisements({ signal: ac.signal })
                                .catch(()=>{
                                    log("您的设备不支持设备电量显示，请留意设备电量")
                                    $("#battery").innerHTML="不可用";
                                })
                            else log("您的设备不支持设备电量显示，请留意设备电量");
                            return new Promise((res,rej)=>{
                                device.gatt.connect()
                                    .then(d=>res(e))
                                    .catch(e=>{
                                        setTimeout(()=>{
                                            device.gatt.connect()
                                                .then(e=>res(e))
                                                .catch(e=>rej(e))
                                        }, 1500);
                                    })
                            })
                        })
                        .then((server) => {
                            log("连接成功，与服务通讯...")
                            this.server = server;
                            return server.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
                        })
                        .then((service) => {
                            log("取读属性...")
                            this.service = service;
                            return service.getCharacteristic("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
                        })
                        .then((charaRead) => {
                            log("取写属性...")
                            this.charaRead = charaRead;
                            charaRead.addEventListener(
                                'characteristicvaluechanged', e => {
                                    this.callback(cmdFactory.decodeRes(e.target.value));
                                }
                            );
                            return this.service.getCharacteristic("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
                        })
                        .then((charaWrite) => {
                            log("设备通道建立全部完成");
                            this.charaWrite = charaWrite;
                            res();
                        })
                        .catch((e) => rej(e));
                })
            },
            send(data, t=0) {
                return new Promise((res, rej) => {
                    const doSend=()=>{
                        if (this.working) {
                            log("有未完成的操作，请稍候...")
                            rej();
                            return;
                        }
                        this.working = true;
                        this.charaRead.startNotifications();
                        this.charaWrite.writeValue(data)
                            .catch((e)=>{
                                this.working=false;
                                log(`第${1+t}次发送失败，重发...`)
                                if(t>2) {rej("重发超出次数限制");return;}
                                setTimeout(()=>{
                                    this.send(data, t+1)
                                        .then((e)=>res(e))
                                        .catch((e)=>rej(e))
                                }, 500)
                            })
                        this.callback = (e) => {
                            bleAdapter.working = false;
                            bleAdapter.charaRead.stopNotifications();
                            if (e == "OK") res(e);
                            else rej(e);
                        }
                    }
                    if(bleAdapter.connected) doSend();
                    else {
                        if(bleAdapter.device) {
                            bleAdapter.connect(Promise.resolve(bleAdapter.device))
                                .then(() => {
                                    doSend();
                                })
                                .catch((e)=>{log("蓝牙连接失败！"+e),rej()})
                        } else rej();
                    }
                })
            }
        }

        const deviceController = {
            openTime: 1800,
            closeTime: 650,
            waitTime: 2000,
            attribute: 0,
            password: "",
            get savedPrefs() {
                return localStorage.savedPrefs ? JSON.parse(localStorage.savedPrefs) : {};
            },
            set savedPrefs(pref) {
                localStorage.savedPrefs = JSON.stringify(pref);
                return
            },
            paramsToUI() {
                $("#deviceId").value=bleAdapter.device?bleAdapter.device.id:"";
                $("#password").value=this.password;
                $("#attribute").value=this.attribute;
                $("#openTime").value=this.openTime;
                $("#closeTime").value=this.closeTime;
                $("#waitTime").value=this.waitTime;
            },
            UIToParams() {
                return {
                    deviceId:$("#deviceId").value,
                    password:$("#password").value,
                    attribute:$("#attribute").value,
                    openTime:$("#openTime").value,
                    closeTime:$("#closeTime").value,
                    waitTime:$("#waitTime").value
                }
            },
            saveCurrent() {
                const currentData = this.UIToParams();
                for(x in currentData) {
                    if(currentData[x]==="") {
                        log("请填写所有项目再保存！")
                        return;
                    }
                }
                const prefs = this.savedPrefs;
                prefs[currentData.deviceId] = currentData;
                this.savedPrefs = prefs;
                log("已保存，您可点击 开锁 按钮。");
                return;
            },
            openLock() {
                return new Promise((res,rej)=>{
                    log("正在开锁...")
                    if(this.getSpecPref()) {
                        bleAdapter.send(cmdFactory.genCmd(this.password, `A:OPEN;P:${this.attribute==1?"-":"+"}${this.openTime},${this.waitTime},${this.closeTime};`))
                            .then((e) => {
                                res()
                                log("开锁成功！" + e);
                                bleAdapter.server.disconnect()
                                localStorage.lastDeviceId=bleAdapter.device.id;
                            })
                            .catch((e) => {
                                rej()
                                log("开锁失败！" + e);
                            })
                    }
                })
            },
            openLockDirect() {
                bleAdapter.connect()
                    .then(() => {
                        this.openLock()
                    })
                    .catch((e)=>log("蓝牙连接失败！"+e))
            },
            openLastLock() {
                let lid;
                if(lid = localStorage.lastDeviceId) {
                    bleAdapter.connect(bleAdapter.getConnectedDeviceByID(lid))
                        .then(() => {
                            this.openLock()
                        })
                        .catch((e)=>log("蓝牙连接失败！"+e))
                } else {
                    log("您必须正常开锁一次后方可使用快速开锁")
                    return;
                }
            },
            connectOnly() {
                bleAdapter.connect()
                    .then(() => {
                        log("设备已连接");
                        this.getSpecPref();
                    })
                    .catch((e)=>log("蓝牙连接失败！"+e))
            },
            getSpecPref() {
                try {
                    const deviceId = bleAdapter.device.id;
                    const savedPrefs = this.savedPrefs;
                    if(deviceId in savedPrefs) {
                        const currentPref = savedPrefs[deviceId];
                        this.openTime = currentPref.openTime;
                        this.waitTime = currentPref.waitTime;
                        this.closeTime = currentPref.closeTime;
                        this.attribute = currentPref.attribute;
                        this.password = currentPref.password;
                        this.paramsToUI();
                        return true;
                    } else {
                        log("未保存设备信息，请在填写设备信息后保存。")
                        this.paramsToUI();
                        return false;
                    }
                } catch { log("蓝牙未连接或连接异常，无法取得设备信息"); }
            },
            changePsw() {
                const ori=$("#password").value;
                const npw=$("#npw").value;
                if(!(ori&&npw)) {
                    log("新旧密码不得为空");
                    return;
                }
                bleAdapter.send(cmdFactory.genCmd(this.password, `A:PW;P:${cmdFactory.md5s(npw)};`))
                    .then((e) => {
                        log("修改成功！" + e);
                        $("#password").value=npw;
                        this.saveCurrent();
                    })
                    .catch((e) => {
                        log("修改失败！" + e);
                    })
            }
        }

        window.onload=(e)=>{
            deviceController.paramsToUI();
            navigator.serviceWorker.register('/smartlock/sw.js', { scope: '/smartlock/' })
				.then(
					function (registration) {
						console.log('ServiceWorker registration successful with scope: ', registration.scope);
					},
					function (err) {
						console.log('ServiceWorker registration failed: ', err);
					}
				);
        }
    </script>
</body>

</html>